// Workspace.java
// by Alexandre Martins, 2013
// MAC 5857 - Prof. Leonidas

package example.ilm.model;

import ilm.framework.assignment.model.AssignmentState;
import ilm.framework.assignment.model.DomainAction;
import ilm.framework.assignment.model.DomainObject;

import example.ilm.gui.Polygon2D;

import java.awt.geom.Point2D;
import java.awt.geom.Point2D.Double;
import java.util.List;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.ListIterator;
import java.util.Observable;
import java.util.Observer;
import java.util.HashMap;

// O Workspace, que e' parte do model, observa AssignmentState (model)
// e e' observado por Canvas (view)
public class Workspace extends Observable implements Observer {

    public final static int VERTEX_SIZE = 10;
    public final static float VERTEX_MARGIN = 3.0f;
    public final static int ROTATIONPOINT_DISTANCE = 40;

    private List geometricElements;
    private GeometricElement focusedObject;

    public Workspace() {
        geometricElements = new ArrayList();
        dragndropStrategy = null;
        focusedObject = null;
    }

    // -----------------------------------------
    // lista de elementos geometricos
    // -----------------------------------------
    public List getGeometricElementList() {
        return geometricElements;
    }

    public void setGeometricElementList(List l) {
        geometricElements = new ArrayList(l);
        notify(geometricElements);
    }

    // -----------------------------------------
    // objeto em foco
    // -----------------------------------------
    public void setFocusedGeometricElement(GeometricElement ge)
    {
        if(ge != focusedObject) { //!ge.equals(focusedObject)) {
            focusedObject = ge;
            notify(focusedObject);
        }
    }

    public void unsetFocusedGeometricElement()
    {
        setFocusedGeometricElement(null);
    }

    public GeometricElement getFocusedGeometricElement()
    {
        return focusedObject;
    }

    // -----------------------------------------
    // localizacao & ordenacao de objetos geometricos
    // -----------------------------------------
    public GeometricElement getGeometricElementAt(int x, int y) {
        return getGeometricElementAt(x, y, new int[1]);
    }

    public GeometricElement getGeometricElementAt(int x, int y, int[] outVertexId) {
        GeometricElement candidate = collisionCheck(focusedObject, x, y, outVertexId);
        if(candidate == null) {
            ListIterator it = geometricElements.listIterator( geometricElements.size() );
            while(it.hasPrevious()) {
                candidate = collisionCheck((GeometricElement)(it.previous()), x, y, outVertexId);
                if(candidate != null)
                    return candidate;
            }
        }

        return candidate;
    }

    public void sendToBack(GeometricElement el) {
        int idx = geometricElements.indexOf(el);
        if(idx >= 0) {
            remove(el);
            geometricElements.add(0, el);
            notify(geometricElements);
        }
    }

    public void bringToFront(GeometricElement el) {
        int idx = geometricElements.indexOf(el);
        if(idx >= 0) {
            remove(el);
            geometricElements.add(el);
            notify(geometricElements);
        }
    }

    public void sendBackwards(GeometricElement el) {
        int idx = geometricElements.indexOf(el);
        if(idx > 0) {
            Object prev = geometricElements.get(idx-1);
            geometricElements.set(idx-1, el);
            geometricElements.set(idx, prev);
            notify(geometricElements);
        }
    }

    public void bringForwards(GeometricElement el) {
        int idx = geometricElements.indexOf(el);
        if(idx >= 0 && idx < geometricElements.size()-1) {
            Object next = geometricElements.get(idx+1);
            geometricElements.set(idx+1, el);
            geometricElements.set(idx, next);
            notify(geometricElements);
        }
    }


    // -----------------------------------------
    // drag & drop
    // -----------------------------------------
    private DragnDropStrategy dragndropStrategy;

    public void drag(int x, int y)
    {
        if(dragndropStrategy == null) {
            int[] sv = new int[1];
            GeometricElement ge = getGeometricElementAt(x, y, sv);
            if(ge != null) {
                if(focusedObject != null) {
                    int selectedVertex = sv[0];
                    if(selectedVertex == ge.getNumberOfVertices())
                        dragndropStrategy = (ge instanceof Polygon) ? new RotateStrategy((Polygon)ge, x, y) : null;
                    else if(selectedVertex >= 0)
                        dragndropStrategy = (ge instanceof Shape) ? new ScaleStrategy((Shape)ge, x, y) : null;
                    else
                        dragndropStrategy = new TranslateStrategy(ge, x, y);
                }
            }
        }

        if(dragndropStrategy != null) {
            dragndropStrategy.drag(x, y);
            dragndropStrategy.getGeometricElement().drag();
            notify(geometricElements);
        }
    }

    public void drop(HashMap actionList)
    {
        if(dragndropStrategy != null) {
            dragndropStrategy.drop(actionList);
            dragndropStrategy.getGeometricElement().drop();
            notify(geometricElements);
        }

        dragndropStrategy = null;
    }




    // -----------------------------------------
    // padrao Observer
    // -----------------------------------------
    public void update(Observable o, Object arg)
    { // recebi atualizacao do AssignmentState
        AssignmentState state = (AssignmentState)o;
        arg = findUpdatedObject(state); // nao precisa mais?
        if(arg != null && (arg instanceof GeometricElement)) { // algum obj foi adicionado ou removido ...
            boolean theObjectHasBeenAdded = (state.getList().indexOf(arg) >= 0);
            if(theObjectHasBeenAdded)
                add((GeometricElement)arg);
            else
                remove((GeometricElement)arg);

            notify(geometricElements);
        }
    }














    private void add(GeometricElement geometricElement)
    {
        geometricElements.add(geometricElement);
    }

    private void remove(GeometricElement geometricElement)
    {
        Iterator it = geometricElements.iterator();
        while(it.hasNext()) {
            GeometricElement ge = (GeometricElement)(it.next());
            if(ge.equals(geometricElement)) {
                geometricElements.remove(ge);
                return;
            }
        }
    }

    private DomainObject findUpdatedObject(AssignmentState state)
    {
        //
        // forca bruta !!!!!
        //

        List herObjects = state.getList();
        List myObjects = geometricElements;

        Iterator it = geometricElements.iterator();
        while(it.hasNext()) {
            DomainObject ge = (DomainObject)(it.next());
            if(herObjects.indexOf(ge) < 0)
                return ge; // removi ge
        }

        it = herObjects.iterator();
        while(it.hasNext()) {
            DomainObject o = (DomainObject)(it.next());
            if(myObjects.indexOf(o) < 0)
                return o; // adicionei o
        }

        return null; // objeto nao foi removido, nem adicionado
    }

    private void notify(Object arg)
    {
        setChanged();
        notifyObservers(arg);
    }

    //
    // verifica se (x,y) colide com ge, e em qual vertice.
    // se nao ha' colisao, devolve null
    //
    private GeometricElement collisionCheck(GeometricElement ge, int x, int y, int[] outVertexId)
    {
        if(ge != null) {
            Point2D p = new Point2D.Double(x, y);
            GeometricElement candidate = null;

            // check if (x,y) is inside the geometric element
            if(ge.contains(p)) {
                outVertexId[0] = -1;
                candidate = ge;
            }

            // check if (x,y) is colliding with a vertex of the
            // geometric element
            Point2D first = null, second = null, last = null;
            Iterator it2 = ge.getTransformedVertices().iterator();
            for(int vid = 0; it2.hasNext(); vid++) {
                Point2D vertex = (Point2D)(it2.next());

                if(0 == vid)
                    first = vertex;
                else if(1 == vid)
                    second = vertex;
                else
                    last = vertex;

                double ax = p.getX();
                double ay = p.getY();
                double bx = vertex.getX();
                double by = vertex.getY();

                boolean gotCollision = (Math.max(Math.abs(ax - bx), Math.abs(ay - by)) <= (VERTEX_SIZE * VERTEX_MARGIN) / 2); // infinity-norm
                if(gotCollision) {
                    outVertexId[0] = vid;
                    candidate = ge;
                    break;
                }
            }

            // check if (x,y) collides with the rotation point
            int numberOfVertices = ge.getTransformedVertices().size();
            if(first != null && second != null && last != null) {
                Point2D midpoint = new Point2D.Double(0.5 * (second.getX() + last.getX()), 0.5 * (second.getY() + last.getY()));
                double directionX = first.getX() - midpoint.getX(), directionY = first.getY() - midpoint.getY();
                double norm = Math.sqrt(directionX * directionX + directionY * directionY);
                if(norm > 1e-5) {
                    directionX /= norm; directionY /= norm;
                    Point2D rotpoint = new Point2D.Double(first.getX() + directionX * ROTATIONPOINT_DISTANCE, first.getY() + directionY * ROTATIONPOINT_DISTANCE);

                    boolean gotCollision = (p.distanceSq(rotpoint) <= ((VERTEX_SIZE * VERTEX_MARGIN) / 2) * ((VERTEX_SIZE * VERTEX_MARGIN) / 2)); // 2-norm
                    if(gotCollision) {
                        outVertexId[0] = numberOfVertices;
                        candidate = ge;
                    }
                }
            }

            // done!
            return candidate;
        }
        else
            return null;
    }
}
